<?php

/**
 * A utility for mixing in multiple classes into an object. Best used for automated
 * class extending. Read the documentation and view the examples for best
 * implementation practices.
 * 
 * This object relies on caching and one-class-per-file rules to accomplish the seemingly
 * impossible. 
 *
 * @package mixinUtil
 * @author David Boskovic
 **/
class e_MixinUtil
{
	
	
	/**
	 * This is an array of file info as previously loaded in the past.
	 *
	 * @var string
	 */
	private static $_stack_index = array();
	
	/**
	 * This is an index of classes as they are built.
	 *
	 * @var array
	 */
	private static $_classes = array();
	private static $_bases = array();
	
	/**
	 * An array of any classes that need to be rebuilt into a stack.
	 *
	 * @var array
	 */
	private static $_rebuild = array();
	
	/**
	 * Configuration Options
	 *
	 * @var array
	 **/
	public static $options = array(
		'base_path' 					=> false,
		'stack_location' 				=> './stacks',
	);
	
	/**
	 * This method is used instead of include() when loading class files.
	 *
	 * @author David Boskovic
	 */
	public static function load($file) {
		
		# make sure the cache has been loaded
		if(!self::$_stack_index) self::$_stack_index = self::_get_cached_stacks();
		if(!self::$_stack_index) self::$_stack_index = array();
		
		$hash = md5($file);
		
		# check to see if this file has been indexed into any stack and whether it needs to be reloaded based on filemtime
		if(isset(self::$_stack_index[$hash]) && filemtime($file) <= self::$_stack_index[$hash]['last_modified']) {
			self::$_classes[self::$_stack_index[$hash]['class']][] = self::$_stack_index[$hash];	
			self::$_bases[self::$_stack_index[$hash]['base_class']][] = self::$_stack_index[$hash];
		}
		else {
			$info = self::_get_info($file);
			self::$_classes[$info['class']][] = $info;	
			self::$_bases[$info['base_class']][] = $info;
			self::$_rebuild[$hash] = 1;
			self::$_stack_index[$hash] = $info;		
			self::_put_cached_stacks(); // make sure our stack cache is saved
		}
	}
	
	/**
	 * Get the info for a file.
	 *
	 * @param string $file 
	 * @return array
	 */
	private static function _get_info($file) {
		$modtime = filemtime($file);
		$src = file_get_contents($file);
		list($ext, $base) = self::_extract_classnames($src);
		return array('last_modified' => $modtime, 'base_class' => $base, 'class' => $ext, 'hash' => md5($file), 'file' => $file);
	}
	
	/**
	 * Extract the classname and extension from a file.
	 * @param string $src
	 * @return array
	 */
	private static function _extract_classnames(&$src) {
		// regex for matching classnames
		$match = "/(class\s(\w+)((\sextends)(\s(\w+)))*\s\{)/";
		
		preg_match($match, $src, $regs);
		$class = $regs[2];
		$extends = isset($regs[6]) ? $regs[6] : false;
		return array($class, $extends);
	}
	
	/**
	 * Load the cached stacks.
	 *
	 * @return mixed
	 */
	private static function _get_cached_stacks() {
		$contents = file_get_contents('stackindex.cache');
		$data = unserialize($contents);
		return $data;
	}
	
	/**
	 * Update the stacks in the cache.
	 *
	 * @return bool
	 */
	private static function _put_cached_stacks() {
		$data = serialize(self::$_stack_index);
		return file_put_contents('stackindex.cache', $data);
	}
	
	/**
	 * Finalize the mix. (no more objects can be added at this point)
	 *
	 * @return bool
	 */
	public static function mix() {
		$used_head = array();
		foreach(self::$_bases as $class => $items) {
			if($class === 0) continue;
			$parent = count(self::$_classes[$class]) == 1 ? self::$_classes[$class][0] : -1;
			if(!self::$_classes[$class]) die('Hey, This is utilMixin. We could not find an instance of <b>'.$class.'</b> to extend.');
			elseif($parent == -1) die('Hey, This is utilMixin. You can\'t have multiple instances of <b>'.$class.'</b> since it is a base class.');
			$used_head[$parent['file']] = 1;
			$include = $parent['hash'];
			$build = array();
			$build[] = $parent;
			$rebuild = 0;
			if(isset(self::$_rebuild[$parent['hash']])) $rebuild =1;
			foreach($items as $item) {
				$include .= $item['hash'];
				if(isset(self::$_rebuild[$item['hash']])) $rebuild =1;
				$build[] = $item;
			}
			$includehash = md5($include);
			if(!file_exists(self::$options['stack_location'].'/'.$includehash.'.php')) $rebuild = 1;
			if($rebuild) {
			
				# open the base file.
				$base_file = file_get_contents($parent['file']);

				# setup the initial class to extend
				$base_class = $parent['class'];
				$this_base = $base_class.'_eXtendBase';
				//$c = $base_file;
				$ct = count($build);
				foreach($build as $key => $item) {
					$c .= "\n\n/**\n * Stacked From [$item[class]($item[base_class]) @ $item[file]]\n */\n";
					$s = file_get_contents($item['file']);
					$s = str_replace('extends '.$base_class, 'extends '.$this_base, $s);
					$this_base = $key +1 == $ct ? $base_class : $item['class'].'_miXin_'.$key;
					$s = str_replace('class '.$item['class'], 'class '.$this_base, $s);
					$c .= $s;
				}
			
				$c = str_replace('<?php', '', $c);
				file_put_contents(self::$options['stack_location'].'/'.$includehash.'.php',"<?php\n// built by PHPmiXin. DO NOT MODIFY\n\n".$c);
			}
			include(self::$options['stack_location'].'/'.$includehash.'.php');
		}
		if(is_array(self::$_bases[0]) && count(self::$_bases[0])) {
			foreach(self::$_bases[0] as $class) {
				if(!isset($used_head[$class['file']]))
					include($class['file']);
			}
		}
	}
	
	
	
} // END class e_MixinUtil